﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Collections.Concurrent;

namespace Utilities
{
    /// <summary>
    /// Extension methods for the type object
    /// </summary>
    public static class TypeExtensions
    {
        /// <summary>
        /// Unwraps the "clean" type from collections and nullable types
        /// </summary>
        /// <param name="type">The type to unwrap</param>
        /// <returns></returns>
        public static Type UnwrapType(this Type type)
        {
            if (type.IsCollection())
            {
                type = type.GetItemType();
            }

            if (type.IsNullableValueType())
            {
                type = type.GetNullableType();
            }

            return type;
        }

        /// <summary>
        /// Retrieves the interface that has a property info that matches the name of the property name
        /// </summary>
        /// <param name="type">The type to test</param>
        /// <param name="propertyName">The name of the property to find</param>
        /// <returns>The interface type if found, otherwise returns null</returns>
        public static Type GetInterfaceForProperty(this Type type, string propertyName)
        {
            return (from Type @interface in type.GetInterfaces()
                    where @interface.GetProperty(propertyName) != null
                    select @interface).FirstOrDefault();
        }

        /// <summary>
        /// Determines whether the type has an interface (generic or not) of the given name
        /// </summary>
        /// <param name="type"></param>
        /// <param name="interfaceName"></param>
        /// <returns></returns>
        public static bool HasSomeInterfaceOf(this Type type, string interfaceName)
        {
            return (from iface in type.GetInterfaces()
                    where iface.Name.StartsWith(interfaceName)
                    select iface).FirstOrDefault() != null;
        }

        /// <summary>
        /// Tests whether this type has a default constructor
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static bool HasDefaultConstructor(this Type type)
        {
            return (type.GetConstructor(Type.EmptyTypes) != null);
        }

        /// <summary>
        /// Tests whether the generic value is null for a reference type or the default primitive value for a struct type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value"></param>
        /// <returns></returns>
        public static bool IsDefault<T>(this T value)
        {
            return EqualityComparer<T>.Default.Equals(value, default(T));
        }

        private static readonly ConcurrentDictionary<Type, object> _defaultValues = new ConcurrentDictionary<Type, object>(); // Cache the default values of the types
        /// <summary>
        /// Retrieves the default value for the type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static object GetDefault(this Type type)
        {
            if (type.IsValueType)
            {
                return _defaultValues.GetOrAdd(type, Activator.CreateInstance(type));
            }

            return null;
        }

        /// <summary>
        /// Retrieves an instance of an object from a string
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static object FromString(this Type type, string value)
        {
            if (type == typeof(string))
            {
                return value;
            }

            if (string.IsNullOrWhiteSpace(value))
            {
                return type.GetDefault();
            }

            if (type.IsEnum)
            {
                return Enum.Parse(type, value);
            }

            if (type.IsNullable())
            {
                type = Nullable.GetUnderlyingType(type);
            }

            // Try the parse method
            MethodInfo parse = type.GetMethod("Parse", new Type[] { typeof(string) });

            if (parse != null) 
            { 
                return parse.Invoke(null, new object[] { value }); 
            }

            return Convert.ChangeType(value, type);
        }
    }
}
